﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TribesPSO
{
    /// <summary>
    /// The base class for all Tribes PSO search spaces
    /// </summary>
    /// <remarks>
    /// There is a lot of default behavior included in the search space object.  For example, by default new particles are 
    /// placed in the search space based on uniform distributions.  Most of the default behaviors can be changed
    /// by child classes.
    /// </remarks>
    public abstract class SearchSpace<TParticle> where TParticle : Particle
    {
        private readonly List<Tribe> tribeList;
        private readonly IHyperspaceRandom rng;
        private readonly IObjectiveFunction goodnessFunction;
        private int movesSinceAdaptation;

        /// <summary>
        /// Raised right before the swarm moves
        /// </summary>
        public event EventHandler SwarmMoving = delegate { };

        /// <summary>
        /// Raised after all of the particles in the tribe have been moved
        /// </summary>
        public event EventHandler SwarmMoved = delegate { };

        /// <summary>
        /// Raised right before the swarm is adapted.
        /// </summary>
        public event EventHandler SwarmAdapting = delegate { };

        /// <summary>
        /// Raised after the swarm has adapted
        /// </summary>
        public event EventHandler SwarmAdapted = delegate { };

        /// <summary>
        /// Initializes a new SearchSpace object to optimize the specified objective function.  This search space
        /// will use the TribesPSO.HyperspaceRandom random number generator to move the particles
        /// </summary>
        /// <param name="objectiveFunction">The objective function to optimize</param>
        protected SearchSpace(IObjectiveFunction objectiveFunction)
            : this(objectiveFunction, new HyperspaceRandom()) { }

        /// <summary>
        /// Initializes a new SearchSpace object to optimize the specified objective function.  This search space
        /// will use the specified random number generator to move the particles
        /// </summary>
        /// <param name="objectiveFunction">The objective function to optimize</param>
        /// <param name="randomNumberGenerator">The random number generator that will be used to move the particles</param>
        protected SearchSpace(IObjectiveFunction objectiveFunction, IHyperspaceRandom randomNumberGenerator)
        {
            if (objectiveFunction == null) throw new ArgumentNullException("objectiveFunction");
            if (randomNumberGenerator == null) throw new ArgumentNullException("randomNumberGenerator");
            this.rng = randomNumberGenerator;
            this.movesSinceAdaptation = 0;
            this.goodnessFunction = objectiveFunction;
            this.tribeList = new List<Tribe>();
        }

        /// <summary>
        /// Gets a list of tribes in the search space
        /// </summary>
        protected IList<Tribe> Tribes { get { return tribeList; } }

        /// <summary>
        /// Gets the random number generated used by the search space
        /// </summary>
        protected IHyperspaceRandom RandomNumberGenerator { get { return rng; } }

        /// <summary>
        /// Gets the Objective function used to evaluate particle performance in the search space
        /// </summary>
        public IObjectiveFunction GoodnessFunction { get { return goodnessFunction; } }

        /// <summary>
        /// Gets the best solution that's been found by the swarm so far.  Will return null if the swarm was just
        /// constructed but MoveThenAdapt() hasn't been called it
        /// </summary>
        public Solution BestSolution
        {
            get
            {
                if (Tribes.Any())
                {
                    return this.Tribes.OrderBy(o => o.BestSolution.Error).First().BestSolution;
                }
                else
                {
                    return null;
                }
            }
        }

        /// <summary>
        /// Gets the number of tribes in the search space
        /// </summary>
        public int TribeCount { get { return this.Tribes.Count(); } }

        /// <summary>
        /// Gets the position of all of the particles in the swarm
        /// </summary>
        public IEnumerable<EuclidianVector> ParticlePositions
        {
            get
            {
                var Positions = from particle in this.Tribes.SelectMany(o => o.TribeMembers)
                                select particle.Position;
                return Positions;
            }
        }

        /// <summary>
        /// Gets the number of particles in the search space
        /// </summary>
        public int SwarmSize { get { return this.Tribes.Sum(o => o.MemberCount); } }

        /// <summary>
        /// Called by MoveThanAdapt().  Implementers should provide logic to move some, or all of the particles
        /// in the swarm.
        /// </summary>
        protected abstract void Move();

        /// <summary>
        /// Called when the swarm needs to add a new particle.  This method provides the new particle at a specified
        /// location
        /// </summary>
        /// <remarks>
        /// The implementation of this method determines the type of particles that make up the swarm.  You can have
        /// this method always generate the same type of particle, or you can pick different particles based on
        /// how well the swarm is doing.
        /// </remarks>
        /// <param name="position">The location to generate the new particle at</param>
        /// <returns>The a new particle located at position</returns>
        protected abstract TParticle GenerateParticleAtPosition(EuclidianVector position);

        /// <summary>
        /// If the swarm is empty, this method will seed the search space based on the behavior of
        /// SeedSearchSpace();
        /// 
        /// If the swarm is not empty, this method Moves the particles in the swarm then
        /// checks if it's time for an adaptation after the move.  If it's time, this method calls
        /// Adapt() to adapt the swarm size.
        /// </summary>
        public void MoveThenAdapt()
        {
            /* We initialize the swarm in this method instead of the constructor to avoid calling virtual
             * methods from within the constructor.  Doing it this way is a little bit more confusing
             * but I think it's worth it because it allows people to inherit from SearchSpace and change how
             * the particles are initially distributed more easily*/
            if (this.SwarmSize == 0)
            {
                SeedSearchSpace();
            }
            else
            {
                SwarmMoving(this, EventArgs.Empty);
                Move();
                this.movesSinceAdaptation += 1;

                //First let all of the tribes know that the swarm has moved
                foreach (var tribe in this.Tribes)
                {
                    tribe.NotifySwarmMoved();
                }
                //Then notify anybody else who wants to know
                SwarmMoved(this, EventArgs.Empty);

                if (NeedsToAdapt(movesSinceAdaptation))
                {
                    SwarmAdapting(this, EventArgs.Empty);
                    Adapt();
                    this.movesSinceAdaptation = 0;

                    //Again, we're going to tell all of the tribes that we've adapted the swarm
                    foreach (var tribe in this.Tribes)
                    {
                        tribe.NotifySwarmAdapted();
                    }
                    //Then we're going to tell anybody else whose subscribed
                    SwarmAdapted(this, EventArgs.Empty);
                }
            }
        }

        /// <summary>
        /// Signals that the swarm is due for an adaptation.  This implementation returns true when the
        /// moves since the last adaptation is greater than or equal to the total number of links in the swarm
        /// divided by four
        /// </summary>
        /// <remarks>
        /// Override this method if you want to change how frequently the swarm adapts its size
        /// </remarks>
        /// <param name="movesSinceLastAdaptation"></param>
        /// <returns></returns>
        protected virtual bool NeedsToAdapt(int movesSinceLastAdaptation)
        {
            int NumberOfLinks = this.Tribes.Sum(o => o.InternalLinkCount + o.ExternalLinkCount);
            int AdaptationInterval = NumberOfLinks / 4;
            return movesSinceLastAdaptation >= AdaptationInterval;
        }

        /// <summary>
        /// Adapts the swarm size.  Good tribes have a chance of killing their best particle.  If there are bad tribes 
        /// in the swarm, a new tribe will be created with one particle per bad tribe.  Each bad tribe will
        /// have the new tribe added as an informer
        /// </summary>
        /// <remarks>
        /// Note to inheritors: Adapt is responsible for removing any empty tribes from the swarm
        /// </remarks>
        protected virtual void Adapt()
        {
            //Kill bad particles in good tribes
            foreach (Tribe t in this.Tribes.Where(o => o.IsGood))
            {
                t.TryRemoveWorstParticle();
            }
            this.tribeList.RemoveAll(o => o.TribeMembers.IsEmpty());

            //Generate a new particle for each tribe that is bad
            var BadTribes = from tribe in this.Tribes
                            where !tribe.IsGood
                            select tribe;
            if (BadTribes.Any())
            {
                Tribe NewTribe = new Tribe(GenerateNewParticle(BadTribes.Count()), this.RandomNumberGenerator);
                foreach (Tribe badTribe in BadTribes)
                {
                    badTribe.AddInformer(NewTribe);
                }

                this.Tribes.Add(NewTribe);
            }
        }

        /// <summary>
        /// Creates a new position inside the search space
        /// </summary>
        /// <remarks>
        /// In the default implementation,  all positions in the search space are equally likely.
        /// The min and max dimension bounds are determined by the IObjectiveFunction that was passed into the
        /// search space's constructor.
        /// </remarks>
        /// <returns> A new euclidian vector within the search space </returns>
        protected virtual EuclidianVector GeneratePosition()
        {
            EuclidianVectorBuilder Position = new EuclidianVectorBuilder();
            for (int n = 0; n < this.GoodnessFunction.Dimensions; n++)
            {
                double range = this.GoodnessFunction.MaxBounds[n] - this.GoodnessFunction.MinBounds[n];
                double randomPosition = (this.RandomNumberGenerator.NextDouble() * range) + this.GoodnessFunction.MinBounds[n];
                Position.AddDimensionData(randomPosition);
            }
            return Position.ToEuclidianVector();
        }

        /// <summary>
        /// Adds a single new particle to the search space.  If the search space's objective function 
        /// has an initial guess, the new particle will be placed there.
        /// </summary>
        protected virtual void SeedSearchSpace()
        {
            TParticle InitialParticle;
            if (this.GoodnessFunction.InitialGuess == null)
            {
                InitialParticle = GenerateNewParticle();
            }
            else
            {
                InitialParticle = GenerateParticleAtPosition(this.GoodnessFunction.InitialGuess);
            }
            Tribe InitialTribe = new Tribe(InitialParticle, this.RandomNumberGenerator);

            //Generate the first tribe
            this.Tribes.Add(InitialTribe);
        }

        /// <summary>
        /// Generates a number of particles  by calling GenerateNewParticle()
        /// </summary>
        /// <param name="numberToGenerate"></param>
        /// <returns></returns>
        protected IEnumerable<TParticle> GenerateNewParticle(int numberToGenerate)
        {
            for (int n = 0; n < numberToGenerate; n++)
            {
                yield return GenerateNewParticle();
            }
        }

        /// <summary>
        /// Creates a new particle.  This function calls GenerateParticlePosition() to get the position of the new
        /// particle and GenerateParticleAtPosition(EuclidianVector) to actually create the particle
        /// </summary>
        /// <remarks>
        /// This method is called by the default impelmentation of Adapt() and SeedSearchSpace()
        /// </remarks>
        /// <returns>
        /// The new particle with a position determined by GeneratePosition() and other details determined
        /// by GenerateParticleAtPosition(EuclidianVector)
        /// </returns>
        protected TParticle GenerateNewParticle()
        {
            EuclidianVector position = GeneratePosition();
            return GenerateParticleAtPosition(position);
        }
    }
}
